(function() {

    var
    $u = juliet.using({
        validationResult : 'juliet.validation.validationResult',
        validationEvent : 'juliet.validation.validationEvent',
        validator : 'juliet.validation.validator',
        uidGenerator : 'juliet.core.uidGenerator-0.1',
        FieldValueHelper : 'juliet.validation.FieldValueHelper-0.1',
        Observer : 'juliet.core.Observer-0.1'
    }),
    ve = $u('validationEvent'),
    vr = $u('validationResult'),
    $ = juliet.jQuery;

    /**
     * Chain of validators
     */
    var ValidatorsChainClass = function() {

        this.uid = null;
        this.manager = null;
        this.metadata = null;
        this.result = vr.UNKNOWN;
        this.active = true;
        this.lastError = null;
        this.validators = { };
        this.inProgressValidator = null;
        this.firstValidator = null;
        this.fieldValueHelper = juliet.createObject($u('FieldValueHelper'));
        this.observer = juliet.createObject($u('Observer'));
        this.errorOutputElement = null;

        this.failedValidator = null;

        this.initialize = function(o) {
            this.uid = o.uid;
            this.manager = o.manager;
            this.metadata = o.metadata;

            this.fieldValueHelper.initialize(this.metadata.fieldSelector);

            if (this.metadata.events) this.observer.subscribe(this.metadata.events);

            this.createValidators();
            this.attachValidationEvents();
        };

        this.reset = function(resetValue) {
            this.result = vr.UNKNOWN;
            this.lastError = null;
            this.hideLastError();

            if (resetValue) $(this.fieldValueHelper.fieldSelector).val('');

            this.fieldValueHelper.reset();
            this.observer.notify('reseted');
        };

        this.createValidators = function() {
            var validatorsMetadata = this.metadata.validators, previousValidator = null, i;

            for (i = 0; i < validatorsMetadata.length; ++i) {
                var
                currentMetadata = validatorsMetadata[i],
                newValidator = $u('validator').factory.create(currentMetadata.name);

                newValidator.initialize({
                    uid : $u('uidGenerator').getUid(),
                    chain : this,
                    metadata : currentMetadata,
                    previous : previousValidator,
                    next : null
                });

                if (previousValidator !== null) previousValidator.next = newValidator;
                previousValidator = newValidator;
                this.validators[newValidator.uid] = newValidator;
            }

            this.initializeFirstValidator();
        };

        this.initializeFirstValidator = function() {
            for (var validatorUid in this.validators) {
                this.firstValidator = this.validators[validatorUid];
                break;
            }
        };

        this.isNotValidatedYet = function() {
            return (this.result == vr.UNKNOWN);
        };

        this.fixBlurEvent = function(o) {
            if (o.validationEvent == ve.FOCUS) {
                this.fieldValueHelper.updateOnFocusValue();
                this.fieldValueHelper.fieldValueModified = false;
            } else if ((o.validationEvent == ve.BLUR) && (this.fieldValueHelper.isDifferentWithFocusValue()
                || this.fieldValueHelper.fieldValueModified)) {
                o.validationEvent = ve.CHANGE;
            }
        };

        this.tryFastValidate = function(o) {
            return (!this.fieldValueHelper.isDifferentWithPreviousValue() && !this.isNotValidatedYet()
                && (o.validationEvent != ve.CHANGE));
        };

        this.stopInProgressValidator = function() {
            if (!this.inProgressValidator) return;
            this.inProgressValidator.cancelValidate();
            this.inProgressValidator = null;
        };

        this.validate = function(o) {
            o = o || { };

            if (!o.continueValidation) {
                this.fieldValueHelper.updateValue();
                this.fixBlurEvent(o);

                if (this.tryFastValidate(o)) return;

                if (o.callFromManager) {
                    this.fieldValueHelper.updateOnFocusValue();
                    this.fieldValueHelper.updatePreviousValue();
                }

                if ((o.validationEvent == ve.KEYUP) || (o.validationEvent == ve.CHANGE)) {
                    this.fieldValueHelper.fieldValueModified = this.fieldValueHelper.isDifferentWithPreviousValue();

                    if (this.fieldValueHelper.fieldValueModified) this.fieldValueHelper.updatePreviousValue();
                }

                this.lastError = null;
                this.stopInProgressValidator();
            }

            this.runValidationCycle(o);
            this.updateValidationResult();

            if ((o.validationEvent == ve.CHANGE) || o.callFromManager || o.continueValidation) {
                if ((this.lastError == null) || this.lastError.suppress) {
                    if (this.result != vr.IN_PROGRESS) this.hideLastError();
                } else {
                    this.showLastError(o.validationEvent);
                }
            }

            o.chain = this;

            this.observer.notify('validated', o);
        };

        this.runValidationCycle = function(o) {
            var currentValidator = o.currentValidator || this.firstValidator;

            this.failedValidator = null;

            while (currentValidator !== null) {
                if (!currentValidator.active) {
                    currentValidator = currentValidator.next;
                    continue;
                }

                if (o.validationEvent && !currentValidator.isSupportValidationEvent(o.validationEvent)) break;

                if ((currentValidator === o.currentValidator) && o.validationResult) {
                    currentValidator.result = o.validationResult;
                } else {
                    currentValidator.validationEvent = o.validationEvent;
                    currentValidator.validate();
                }

                if (currentValidator.result == vr.FAILED) {
                    this.failedValidator = currentValidator;
                    this.lastError = currentValidator.getError();
                    return;
                } else if (currentValidator.result == vr.IN_PROGRESS) {
                    this.inProgressValidator = currentValidator;
                    return;
                }

                currentValidator = currentValidator.next;
            }
        };

        this.attachValidationEvents = function() {
            var self = this;

            $(this.fieldValueHelper.fieldSelector).each(function() {
                var fieldType = this.type, bindEvents;

                if ((fieldType == 'checkbox') || (fieldType == 'radio') || (fieldType == 'file')) bindEvents = 'change';
                else bindEvents = 'keyup focus blur';

                $(this).bind(bindEvents, function(e) {
                    if (!self.active) return;
                    self.validate({ validationEvent : ve.parse(e.type) });
                });
            });
        };

        this.updateValidationResult = function() {
            var validatorUid, result = 0;
            for (validatorUid in this.validators) result |= this.validators[validatorUid].result;
            this.result = vr.simplifyResult(result);
        };

        this.showLastError = function(validationEvent) {
            var wrongEvent = (validationEvent && (validationEvent != ve.CHANGE));

            if (this.lastError.suppress || (!this.lastError.force && wrongEvent)) return;

            this.getErrorOutputElement().text(this.lastError.text);
            this.getErrorOutputElement().show();
        };

        this.hideLastError = function() {
            this.getErrorOutputElement().hide();
            this.getErrorOutputElement().text('');
        };

        this.getErrorOutputElement = function() {
            if (!this.errorOutputElement) {
                this.errorOutputElement = this.manager.getErrorOutputElement($(this.fieldValueHelper.fieldSelector));
            }

            return this.errorOutputElement;
        };

    };

    juliet.registerModule('juliet.validation', 'ValidatorsChain-0.1', ValidatorsChainClass);

})();
